﻿using System.Reflection;
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Collections.Generic;
using EmbeddedLanguage.Attributes;
using EmbeddedLanguage.SubProcessors;

namespace EmbeddedLanguage
{
    public class AssemblyToJs
    {
        

        public static JSNamespace Generate(Assembly assembly)
        {
            
            var namespaces = new Dictionary<string, JSNamespace>();
            foreach (Type typ in assembly.GetTypes())
            {
                #region Get Static Expressions
                string className = typ.Name.SubstringBefore("`");
                //var methodBindings = typ.GetAttribute<MethodBindingAttribute>();
                var staticFuncsExpressionField = typ.GetField("__" + className, BindingFlags.NonPublic | BindingFlags.Static);
                if (staticFuncsExpressionField != null)
                {
                    Expression staticFuncsExpressions = null;
                    if (typ.IsGenericType)
                    {
                        #region See http://www.codeproject.com/Questions/246278/Using-reflection-on-a-abstract-class-with-genericp
                        var genericArgs = typ.GetGenericArguments();
                        List<Type> types = new List<Type>();
                        for (int i = 0; i < genericArgs.Length; i++)
                        {
                            types.Add(typeof(object));
                        }
                        Type constructed = typ.MakeGenericType(types.ToArray());
                        var staticFuncsExpressionFieldGeneric  = constructed.GetField("__" + className, BindingFlags.NonPublic | BindingFlags.Static);
                        staticFuncsExpressions = staticFuncsExpressionFieldGeneric.GetValue(null) as Expression;
                        #endregion
                    }
                    else
                    {
                        staticFuncsExpressions = staticFuncsExpressionField.GetValue(null) as Expression;
                    }
                    if (staticFuncsExpressions != null)
                    {
                        var smv = new StaticMappingVisitor();
                        smv.Visit(staticFuncsExpressions);
                        #region Build Flat Collection
                        foreach (var kvp in smv.StaticExpressions)
                        {
                            string expFieldName = kvp.Key;
                            string compiledFuncFieldName = kvp.Value;
                            var fi = typ.GetField(expFieldName, BindingFlags.NonPublic | BindingFlags.Static);
                            if (fi == null)
                            {
                                fi = typ.GetField(expFieldName, BindingFlags.Public | BindingFlags.Static);
                            }
                            if (fi == null) throw new Exception("Cannot get field " + expFieldName);
                            #region check if this is a field expression or explicit field
                            string fnName = compiledFuncFieldName;
                            if (smv.MethodAssociations.ContainsKey(fnName))
                            {
                                fnName = smv.MethodAssociations[fnName];
                            }
                            var JSExplicit = fi.GetAttribute<JSExplicitScriptAttribute>();
                            if (JSExplicit != null)
                            {
                                #region explicit js function
                                var fn = new JSFunction
                                {
                                    FunctionName = fnName,
                                    ExplicitScript = JSExplicit.Script,
                                };
                                string ns = ExpressionToJSProcessor.GetFullName(typ);
                                if (!namespaces.ContainsKey(ns))
                                {
                                    namespaces[ns] = new JSNamespace
                                    {
                                        Path = ns
                                    };
                                }
                                var jsNS = namespaces[ns];
                                jsNS.FunctionsNN.Add(fn);
                                #endregion
                                continue;

                            }


                            #region Field Is Static
                            if (!typ.IsGenericType)
                            {
                                #region Typ is not generic
                                var expr = fi.GetValue(null) as Expression;

                                if (expr != null)
                                {
                                    string ns = ExpressionToJSProcessor.GetFullName(typ);
                                    if (!namespaces.ContainsKey(ns))
                                    {
                                        namespaces[ns] = new JSNamespace
                                        {
                                            Path = ns
                                        };
                                    }
                                    var jsNS = namespaces[ns];

                                    var fn = new JSFunction
                                    {
                                        FunctionName = fnName,
                                        Expression = expr,
                                    };
                                    jsNS.FunctionsNN.Add(fn);
                                }
                                #endregion
                            }
                            #endregion

                            #endregion
                        }
                        
                            
                        
                        #endregion
                    }
                }
                #endregion
                #region Get Member Method to Static Function Bindings
                if (typeof(IHaveMethodBindExpression).IsAssignableFrom(typ) && !typ.IsAbstract)
                {
                    var obj = Activator.CreateInstance(typ) as IHaveMethodBindExpression;
                    var visitor = new Method2StaticFunctionMappingProcessor();
                    visitor.Visit(obj.GetMethodBind());
                }
                #endregion
            }
            var tree = ToNameSpaceTree(namespaces);
            tree.Assembly = assembly;
            return tree;
        }

        

        private static JSNamespace ToNameSpaceTree(Dictionary<string, JSNamespace> FlatCollection)
        {
            var returnObj = new JSNamespace();
            foreach (var kvp in FlatCollection)
            {
                var ns = kvp.Value;
                string[] pathTokens = ns.Path.Split('.');
                var pointer = returnObj;
                string cumPath = string.Empty;
                foreach (string path in pathTokens)
                {
                    if (cumPath != string.Empty)
                    {
                        cumPath += ".";
                    }
                    cumPath += path;
                    var child = pointer.ChildrenNN.Find(m => m.Path.StartsWith(cumPath));
                    if (child == null)
                    {
                        var newNS = new JSNamespace
                        {
                            Path = cumPath,
                            Parent = pointer,
                        };
                        pointer.Children.Add(newNS);
                        child = newNS;
                    }
                    if (cumPath == ns.Path)
                    {
                        child.Functions = ns.Functions;
                    }
                    pointer = child;
                }
            }
            return returnObj;
        }
    }
}
